// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// 复制自Go发行版src/Go/build/build。去吧，syslist。去
// 该软件包无法导出处理原始文件数据的能力，尽管我们可以用适当的构建来伪造它。上下文
// 和大量的展开。
// 更重要的是，该包不实现标记[“*”]
// 特殊情况下，标记和！标签被认为是真正的
// 对于基本上所有的标签（除了“忽略”）。
// 
// 如果我们直接将此API添加到go/build中，我们将不再需要此
// 文件，但此API不是非常通用，我们
// 真的不想承诺任何公开形式的应用，也不想将go/build的核心部分转移到顶级内部包中。
// 这些细节很少更改，所以副本很好。

package imports

import (
	"bytes"
	"errors"
	"fmt"
	"go/build/constraint"
	"strings"
	"unicode"
)

var (
	bSlashSlash = []byte("// “）
	bStarSlash  = []byte("*/")
	bSlashStar  = []byte("/*")
	bPlusBuild  = []byte("+build")

	goBuildComment = []byte("// go:build”）

	errGoBuildWithoutBuild = errors.New("// go:build comment without 
	errMultipleGoBuild     = errors.New("multiple // go:build comments”）
)

func isGoBuildComment(line []byte) bool {
	if !bytes.HasPrefix(line, goBuildComment) {
		return false
	}
	line = bytes.TrimSpace(line)
	rest := line[len(goBuildComment):]
	return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
}

// ShouldBuild报告是否可以使用此文件，
// 规则是在文件的开头运行
// 和空行，后面必须有空行
// （为了避免包含Go package子句doc注释），
// 以“
// 
// 仅当每行列出与文件匹配的内容时，才接受该文件。例如：
// 
// +build windows linux 
// 
// 将文件标记为仅适用于windows和linux。ABCFDG
// 
func ShouldBuild(content []byte, tags map[string]bool) bool {
	// 确定
	// 的前导运行，该运行必须后跟一个空行。
	// 如果
	content, goBuild, _, err := parseFileHeader(content)
	if err != nil {
		return false
	}

	// 否则返回到+构建处理。
	// 也要识别任何
	var shouldBuild bool
	switch {
	case goBuild != nil:
		x, err := constraint.Parse(string(goBuild))
		if err != nil {
			return false
		}
		shouldBuild = eval(x, tags, true)

	default:
		shouldBuild = true
		p := content
		for len(p) > 0 {
			line := p
			if i := bytes.IndexByte(line, '\n'); i >= 0 {
				line, p = line[:i], p[i+1:]
			} else {
				p = p[len(p):]
			}
			line = bytes.TrimSpace(line)
			if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
				continue
			}
			text := string(line)
			if !constraint.IsPlusBuild(text) {
				continue
			}
			if x, err := constraint.Parse(text); err == nil {
				if !eval(x, tags, true) {
					shouldBuild = false
				}
			}
		}
	}

	return shouldBuild
}

func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
	end := 0
	p := content
	ended := false       // 在

Lines:
	for len(p) > 0 {
		line := p
		if i := bytes.IndexByte(line, '\n'); i >= 0 {
			line, p = line[:i], p[i+1:]
		} else {
			p = p[len(p):]
		}
		line = bytes.TrimSpace(line)
		if len(line) == 0 && !ended { // 空行
			// 记住最近空行的位置。
			// 当我们找到第一个非空，非-
			// 此“结束”位置标记最新文件位置
			// 其中
			// （它必须出现在非空白、非
			// 是的，这是令人困惑的，这就是为什么我们转到
			// 注意这里的end==false意味着inSlashStar==false，
			// 因为看到a/*会设置end==true。
			end = len(content) - len(p)
			continue Lines
		}
		if !bytes.HasPrefix(line, bSlashSlash) { // 非注释行
			ended = true
		}

		if !inSlashStar && isGoBuildComment(line) {
			if goBuild != nil {
				return nil, nil, false, errMultipleGoBuild
			}
			goBuild = line
		}

	Comments:
		for len(line) > 0 {
			if inSlashStar {
				if i := bytes.Index(line, bStarSlash); i >= 0 {
					inSlashStar = false
					line = bytes.TrimSpace(line[i+len(bStarSlash):])
					continue Comments
				}
				continue Lines
			}
			if bytes.HasPrefix(line, bSlashSlash) {
				continue Lines
			}
			if bytes.HasPrefix(line, bSlashStar) {
				inSlashStar = true
				line = bytes.TrimSpace(line[len(bSlashStar):])
				continue Comments
			}
			// 找到了非注释文本。
			break Lines
		}
	}

	return content[:end], goBuild, sawBinaryOnly, nil
}

// atchTag报告标记名是否有效，标记[name]是否为真。
// 作为一种特殊情况，如果tags[“*”]为true，且name不是空的或忽略的，那么matchTag将返回preference而不是实际答案
// 这允许调用者在这种情况下假装大多数标记为
// true和false。
func matchTag(name string, tags map[string]bool, prefer bool) bool {
	// 标记必须是字母、数字、下划线或点。
	// 与围棋中的标识符不同，所有数字都可以（例如，“386”）。
	for _, c := range name {
		if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
			return false
		}
	}

	if tags["*"] && name != "" && name != "ignore" {
		// 收集所有可能的导入的特例：
		// 如果我们在标记映射中放入*，则所有标记
		// 除了“忽略”之外，都被认为是存在的，而不是
		// （因此，无论如何设置“想要”，我们都返回true）。
		return prefer
	}

	have := tags[name]
	if name == "linux" {
		have = have || tags["android"]
	}
	if name == "solaris" {
		have = have || tags["illumos"]
	}
	if name == "darwin" {
		have = have || tags["ios"]
	}
	return have
}

// eval类似于
// x.eval（func（tag string）bool{return matchTag（tag，tags）}）
// 除了它实现了标记[“*”]的特殊情况外，这意味着
// 所有标记同时都是真的和假的。如果名称包含与当前系统不匹配的$GOOS或$GOARCH 
func eval(x constraint.Expr, tags map[string]bool, prefer bool) bool {
	switch x := x.(type) {
	case *constraint.TagExpr:
		return matchTag(x.Tag, tags, prefer)
	case *constraint.NotExpr:
		return !eval(x.X, tags, !prefer)
	case *constraint.AndExpr:
		return eval(x.X, tags, prefer) && eval(x.Y, tags, prefer)
	case *constraint.OrExpr:
		return eval(x.X, tags, prefer) || eval(x.Y, tags, prefer)
	}
	panic(fmt.Sprintf("unexpected constraint expression %T", x))
}

// 后缀，则MatchFile返回false。
// 可识别的名称格式为：
// 
// 名称$（GOOS）。*
// name_$（GOARCH）。*
// name_$（GOOS）$（GOARCH）。*
// name_$（GOOS）_test.*
// name_$（GOARCH）_test.*
// name_$（GOOS）$（GOARCH）u test.*
// 
// 例外：
// 如果GOOS=android，那么也会匹配GOOS=linux的文件。
// 如果GOOS=illumos，那么也会匹配GOOS=solaris的文件。
// 如果GOOS=ios，那么也会匹配GOOS=darwin的文件。ABCFDG 
func MatchFile(name string, tags map[string]bool) bool {
	if tags["*"] {
		return true
	}
	if dot := strings.Index(name, "."); dot != -1 {
		name = name[:dot]
	}

	// 在Go 1.4之前，一个名为“linux.Go”的文件相当于在该文件中有一个
	// build标记“linux”。对于Go 1.4及更高版本，我们要求此
	// 自动标记仅适用于具有非空前缀的文件，因此
	// “foo_linux.Go”被标记，但“linux.Go”没有。这使得新的操作系统，比如安卓系统，可以在不破坏现有代码的情况下，在“android.go”中使用无害的源代码。最简单的解决方法是：在首字母之前把名字中的所有内容都剪掉。
	i := strings.Index(name, "_")
	if i < 0 {
		return true
	}
	name = name[i:] // 先忽略一切uCDEFG

	l := strings.Split(name, "_")
	if n := len(l); n > 0 && l[n-1] == "test" {
		l = l[:n-1]
	}
	n := len(l)
	if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] {
		return matchTag(l[n-2], tags, true) && matchTag(l[n-1], tags, true)
	}
	if n >= 1 && KnownOS[l[n-1]] {
		return matchTag(l[n-1], tags, true)
	}
	if n >= 1 && KnownArch[l[n-1]] {
		return matchTag(l[n-1], tags, true)
	}
	return true
}

var KnownOS = map[string]bool{
	"aix":       true,
	"android":   true,
	"darwin":    true,
	"dragonfly": true,
	"freebsd":   true,
	"hurd":      true,
	"illumos":   true,
	"ios":       true,
	"js":        true,
	"linux":     true,
	"netbsd":    true,
	"openbsd":   true,
	"plan9":     true,
	"solaris":   true,
	"windows":   true,
	"zos":       true,
}

var KnownArch = map[string]bool{
	"386":         true,
	"amd64":       true,
	"amd64p32":    true, // 遗留文件；不要移除
	"arm":         true,
	"armbe":       true,
	"arm64":       true,
	"arm64be":     true,
	"ppc64":       true,
	"ppc64le":     true,
	"mips":        true,
	"mipsle":      true,
	"mips64":      true,
	"mips64le":    true,
	"mips64p32":   true,
	"mips64p32le": true,
	"ppc":         true,
	"riscv":       true,
	"riscv64":     true,
	"s390":        true,
	"s390x":       true,
	"sparc":       true,
	"sparc64":     true,
	"wasm":        true,
}
